
#include <dodoco.h>

#include <glx_shader.h>
#include <cmath>
#include <iostream>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#define STB_IMAGE_IMPLEMENTATION
#include "./stb_image.h"

#include "ddc_io.h"
#include "ddc_mouse.h"
#include "ddc_time.h"

// ANDROID STUFFS...

#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>

AAssetManager* g_assetMgr = nullptr;

extern "C"
JNIEXPORT void JNICALL
Java_com_specdot_dodocodroid_MainActivity_setAssetManager(JNIEnv *env, jclass thiz, jobject mgr) {

    g_assetMgr = AAssetManager_fromJava(env, mgr);
}

std::vector<char> LoadAssetToMemory(const char* path) {
    std::vector<char> buffer;
    if (!g_assetMgr) {
        ddc_LogPrintFmt(ddc::ELogLevel::ERROR, "no global asset manager!");
        return buffer;
    }

    AAsset* asset = AAssetManager_open(g_assetMgr, path, AASSET_MODE_BUFFER);
    if (!asset) {
        ddc_LogPrintFmt(ddc::ELogLevel::ERROR, "cannot load asset: {}", path);
        return buffer;
    }

    size_t len = AAsset_getLength(asset);
    buffer.resize(len);

    ddc_LogPrintFmt(ddc::ELogLevel::ERROR, "loaded asset: {}, size = {}", path, len);

    AAsset_read(asset, buffer.data(), len);
    AAsset_close(asset);

    return buffer;
}

/////////////////////

#define ATLAS_SIDE 16

static float u_unit = 1.0 / ATLAS_SIDE;
static float v_unit = 1.0 / ATLAS_SIDE;

#define UI(x) (ATLAS_SIDE - 1 - (x))
#define VI(x) (ATLAS_SIDE - 1 - (x))

#define ATLAS_UV(x) ((float)((x) % ATLAS_SIDE) * u_unit), ((float) (ATLAS_SIDE - 1 - ((x) / (int) ATLAS_SIDE)) * v_unit)

static float s_quad[] = {
        -0.5f,  0.5f, 0.0f, 0.0f, 1.0f,  // top left
        0.5f,  0.5f, 0.0f, 1.0f, 1.0f,  // top right
        0.5f, -0.5f, 0.0f, 1.0f, 0.0f,  // bottom left
        -0.5f, -0.5f, 0.0f, 0.0f, 0.0f,  // bottom right
};

static unsigned int s_quad_idx[] = {
        0, 2, 1,
        0, 3, 2
};

using namespace ddc::math;

class CubeMesh {
public:
    std::vector<ddc::math::Vec3> m_positions;
    std::vector<ddc_uint32> m_indices;
    std::vector<ddc::math::Vec2> m_uvs;
    std::vector<ddc::math::Vec3> m_normals;

    void SetUv(float uv_x, float uv_y, float uv_w, float uv_h) {
        _m_cur_uv_origin.x = uv_x;
        _m_cur_uv_origin.y = uv_y;

        _m_cur_uv_shape.x = uv_w;
        _m_cur_uv_shape.y = uv_h;
    }

    void AddFace(ddc::math::Vec3 origin, ddc::math::Vec3 up, ddc::math::Vec3 right) {
        auto idx = m_positions.size();

        m_positions.push_back(origin);
        m_positions.push_back(origin + right);
        m_positions.push_back(origin + right + up);
        m_positions.push_back(origin + up);

        // normal for each vertex...
        Vec3 norm = Vec3::cross(right, up);
        m_normals.push_back(norm);
        m_normals.push_back(norm);
        m_normals.push_back(norm);
        m_normals.push_back(norm);

        m_indices.push_back(idx);
        m_indices.push_back(idx + 1);
        m_indices.push_back(idx + 2);
        m_indices.push_back(idx);
        m_indices.push_back(idx + 2);
        m_indices.push_back(idx + 3);

        m_uvs.push_back(_m_cur_uv_origin);
        m_uvs.push_back(_m_cur_uv_origin + ddc::math::Vec2{_m_cur_uv_shape.x, 0});
        m_uvs.push_back(_m_cur_uv_origin + _m_cur_uv_shape);
        m_uvs.push_back(_m_cur_uv_origin + ddc::math::Vec2{0, _m_cur_uv_shape.y});
    }

    void Clear() {
        m_positions.clear();
        m_indices.clear();
        m_uvs.clear();

        m_positions.shrink_to_fit();
        m_indices.shrink_to_fit();
        m_uvs.shrink_to_fit();
    }

    size_t GetPositionsDataSize() const {
        return m_positions.size() * sizeof(Vec3);
    }

    size_t GetIndicesDataSize() const {
        return m_indices.size() * sizeof(uint32_t);
    }

    size_t GetUVsDataSize() const {
        return m_uvs.size() * sizeof(Vec2);
    }

    size_t GetNormalsDataSize() const {
        return m_normals.size() * sizeof(Vec3);
    }

private:
    ddc::math::Vec2 _m_cur_uv_origin;
    ddc::math::Vec2 _m_cur_uv_shape;
};

static class CubeDemo final : public ddc::imgui::ImGuiApp {
    struct {
        glm::mat4 model = glm::mat4(1.0f);
        glm::mat4 vp = glm::mat4(1.0f);
        Vec3 light_dir = Vec3(-1, 1, 1);
        float expand_factor = 1.0f;
    } _m_cube_uniforms {};

    struct {
        glm::mat4 model = glm::mat4(1.0f);
        glm::vec4 quad_effect = glm::vec4(1.0f, 0.0f, 1.0f, 0.0f);
        glm::vec4 quad_uv_disturb = glm::vec4(0.0, 0.3f, 2.0f, 0);
    } _m_quad_uniforms {};

    CubeMesh _m_cube_mesh;

    ddc::graphics::HVertexBuffer _m_cube_vb_pos{}, _m_cube_vb_uv{}, _m_cube_vb_norm{}, _m_quad_vb_pos{};

    ddc::graphics::HIndexBuffer _m_cube_ib = {}, _m_quad_ib = {};
    ddc::graphics::HShaderProgram _m_cube_shader = {};
    ddc::graphics::HShaderProgram _m_cube_shader_outline = {};
    ddc::graphics::HRenderPass _m_cube_render_pass_to_screen = {};
    ddc::graphics::HVertexInput _m_cube_vertex_input = {}, _m_quad_vertex_input = {};
    ddc::graphics::HGraphicsPipeline _m_cube_pipeline = {};
    ddc::graphics::HGraphicsPipeline _m_cube_pipeline_outline = {};
    ddc::graphics::HTexture _m_cube_texture = {};

    ddc::graphics::HTextureSampler _m_tex_sampler = {};

    ddc::graphics::HTexture _m_render_texture_color = {};
    ddc::graphics::HTexture _m_render_texture_ds = {};
    ddc::graphics::HRenderPass _m_render_pass_offscreen = {};

    ddc::graphics::HShaderPropertySet _m_cube_property_set_uniform = {};
    ddc::graphics::HShaderPropertySet _m_cube_property_set_sampler = {};

    ddc::graphics::HShaderPropertySet _m_quad_property_set = {};
    ddc::graphics::HShaderPropertySet _m_quad_property_set_ds_tex = {};

    ddc::graphics::HGraphicsPipeline _m_quad_pipeline = {};
    ddc::graphics::HShaderProgram _m_quad_shader_program = {};

    float _local_rot_x = 30;
    float _local_rot_y = 0;
    float _player_z = 7;

    float _m_rot_speed_multiplier = 1.0f;

    ddc_int32 mx{}, my{}, lmx{}, lmy{};
    ddc_int32 mouse_update_frame{};

    const char* vendor {nullptr};
    const char* renderer {nullptr};
    const char* version {nullptr};

    bool _m_touch_down = false;
    bool _m_gui_captured_mouse = false;

    glm::vec3 _m_quad_scale = glm::vec3(1.0f);
    glm::vec3 _m_quad_wave = glm::vec3(1, 0, 1);

    float _m_sys_time = 0.0f;

    Color _m_clear_color = Color(0.0f, 0.5f, 0.511f, 1.0f);

    bool _m_render_single_quad = true;

    int _m_quad_tex_w = 2048, _m_quad_tex_h = 2048;

public:
    static std::vector<char> LoadFileString(const char *filename) {
        auto contents = LoadAssetToMemory(filename);

        contents.push_back(0);

        return contents;
    }

    template <typename T>
    void CheckHandle(T&& handle) {
        if (IsInvalidHandle(std::forward<T>(handle))) {
            DDC_CRASH_LOC1F("%s", ddc::graphics::GetErrorString());
        }
    }

    void CheckStatus(ddc::Status s) {
        if (s) {
            throw std::runtime_error(ddc::status::GetMessage(s));
        }
    }

    void BuildCubeMesh() {
        /* ROUND */
        _m_cube_mesh.SetUv(ATLAS_UV(3), u_unit, v_unit);
        // X+
        _m_cube_mesh.AddFace({0.5f, -0.5f, 0.5f}, Vec3::up, Vec3::back);
        // X-
        _m_cube_mesh.AddFace({-0.5f, -0.5f, -0.5f}, Vec3::up, Vec3::forward);
        // Z+
        _m_cube_mesh.AddFace({-0.5f, -0.5f, 0.5f}, Vec3::up, Vec3::left);
        // Z-
        _m_cube_mesh.AddFace({0.5f, -0.5f, -0.5f}, Vec3::up, Vec3::right);

        /* TOP */
        _m_cube_mesh.SetUv(ATLAS_UV(0), u_unit, v_unit);
        // Y+
        _m_cube_mesh.AddFace({-0.5f, 0.5f, 0.5f}, Vec3::back, Vec3::left);

        /* BUTTOM */
        _m_cube_mesh.SetUv(ATLAS_UV(2), u_unit, v_unit);
        // Y-
        _m_cube_mesh.AddFace({-0.5f, -0.5f, -0.5f}, Vec3::forward, Vec3::left);
    }

    void BuildTexture() {
        ddc::graphics::TextureDescriptor desc = {};
        int img_w, img_h, n_comp;

        stbi_set_flip_vertically_on_load(true);

        auto tex_data = LoadAssetToMemory("assets/terrain.png");

        ddc_LogPrintFmt(ddc::ELogLevel::ERROR, "tex data size: {}", tex_data.size());

        stbi_set_flip_vertically_on_load(true);
        auto *data = stbi_load_from_memory(
                (unsigned char*)tex_data.data(), tex_data.size(), &img_w, &img_h, &n_comp, 0);

        if (!data) {
            throw std::runtime_error("failed to load texture");
        }

        if (n_comp == 4) {
            desc.format = ddc::graphics::ETexturePixelFormat::RGBA8_UNORM;
        } else if (n_comp == 3) {
            desc.format = ddc::graphics::ETexturePixelFormat::RGB8_UNORM;
        } else if (n_comp == 1) {
            desc.format = ddc::graphics::ETexturePixelFormat::R8_UNORM;
        }

        desc.width = img_w;
        desc.height = img_h;
        desc.init_data.MakeRef(data, img_w * img_h * n_comp * 4);
        desc.sampler_state.filter_mode = ddc::graphics::ETextureSamplerFilterMode::NEAREST;

        _m_cube_texture = ddc::graphics::CreateTexture(desc);

        ddc::graphics::TextureSamplerDescriptor sampler_desc{};
        sampler_desc.opt_texture_create_from = &_m_cube_texture;

        _m_tex_sampler = ddc::graphics::CreateTextureSampler(sampler_desc);

        stbi_image_free(data);

        // OFFSCREEN RENDER TEXTURE

        ddc::graphics::TextureDescriptor rd_tex_desc{};
        rd_tex_desc.width = _m_quad_tex_w;
        rd_tex_desc.height = _m_quad_tex_h;
        rd_tex_desc.format = ddc::graphics::ETexturePixelFormat::RGB8_UNORM;

        _m_render_texture_color = ddc::graphics::CreateTexture(rd_tex_desc);
        CheckHandle(_m_render_texture_color);

        rd_tex_desc.format = ddc::graphics::ETexturePixelFormat::DEPTH24_STENCIL8;
        _m_render_texture_ds = ddc::graphics::CreateTexture(rd_tex_desc);
        CheckHandle(_m_render_texture_ds);
    }

    void BuildCubeVBO() {
        auto& indices = _m_cube_mesh.m_indices;

        ddc::graphics::VertexBufferCreateInfo create_cube_vb_pos_info{};
        create_cube_vb_pos_info.descriptor.static_draw = true;
        create_cube_vb_pos_info.enable_init_update = true;
        create_cube_vb_pos_info.init_update_info.memory.MakeRef(
                _m_cube_mesh.m_positions.data(), _m_cube_mesh.GetPositionsDataSize());
        create_cube_vb_pos_info.init_update_info.rw_size = ddc::graphics::BUFFER_WHOLE_SIZE;

        _m_cube_vb_pos = ddc::graphics::CreateVertexBuffer(create_cube_vb_pos_info);
        CheckHandle(_m_cube_vb_pos);

        ddc::graphics::VertexBufferCreateInfo create_cube_vb_uv_info{};
        create_cube_vb_uv_info.descriptor.static_draw = true;
        create_cube_vb_uv_info.enable_init_update = true;
        create_cube_vb_uv_info.init_update_info.memory.MakeRef(
                _m_cube_mesh.m_uvs.data(), _m_cube_mesh.GetUVsDataSize());
        create_cube_vb_uv_info.init_update_info.rw_size = ddc::graphics::BUFFER_WHOLE_SIZE;

        _m_cube_vb_uv = ddc::graphics::CreateVertexBuffer(create_cube_vb_uv_info);
        CheckHandle(_m_cube_vb_uv);

        ddc::graphics::VertexBufferCreateInfo create_cube_vb_norm_info{};
        create_cube_vb_norm_info.descriptor.static_draw = true;
        create_cube_vb_norm_info.enable_init_update = true;
        create_cube_vb_norm_info.init_update_info.memory.MakeRef(
                _m_cube_mesh.m_normals.data(), _m_cube_mesh.GetNormalsDataSize());
        create_cube_vb_norm_info.init_update_info.rw_size = ddc::graphics::BUFFER_WHOLE_SIZE;

        _m_cube_vb_norm = ddc::graphics::CreateVertexBuffer(create_cube_vb_norm_info);
        CheckHandle(_m_cube_vb_norm);

        ddc::graphics::IndexBufferCreateInfo create_cube_ib_info{};
        create_cube_ib_info.descriptor.static_draw = true;
        create_cube_ib_info.enable_init_update = true;
        create_cube_ib_info.init_update_info.memory.MakeRef(indices.data(), indices.size() * sizeof(uint32_t));
        create_cube_ib_info.init_update_info.rw_size = ddc::graphics::BUFFER_WHOLE_SIZE;

        _m_cube_ib = ddc::graphics::CreateIndexBuffer(create_cube_ib_info);
        CheckHandle(_m_cube_ib);

        ddc::graphics::VertexInputLayout cube_vertex_layout{};
        cube_vertex_layout.binding_state.emplace_back();
        cube_vertex_layout.binding_state.emplace_back();
        cube_vertex_layout.binding_state.emplace_back();

        cube_vertex_layout.binding_state[0]
                .AddAttrib(
                        ddc::graphics::EVertexAttribType::FLOAT32,
                        ddc::graphics::EVertexAttribSematic::POSITION,
                        3, 0)
                .AutoCalcStride();

        cube_vertex_layout.binding_state[1]
                .AddAttrib(
                        ddc::graphics::EVertexAttribType::FLOAT32,
                        ddc::graphics::EVertexAttribSematic::TEXCOORD0,
                        2, 0)
                .AutoCalcStride();

        cube_vertex_layout.binding_state[2]
                .AddAttrib(
                        ddc::graphics::EVertexAttribType::FLOAT32,
                        ddc::graphics::EVertexAttribSematic::NORMAL,
                        3, 0)
                .AutoCalcStride();

        std::array<ddc::graphics::HVertexBuffer, 3> vb_handles = { _m_cube_vb_pos, _m_cube_vb_uv, _m_cube_vb_norm };

        ddc::graphics::VertexInputDescriptor cube_vertex_input_descriptor{};
        cube_vertex_input_descriptor.p_layout = &cube_vertex_layout;
        cube_vertex_input_descriptor.p_vertex_buffer = vb_handles.data();
        cube_vertex_input_descriptor.n_vertex_buffers = vb_handles.size();
        cube_vertex_input_descriptor.p_index_buffer = &_m_cube_ib;

        _m_cube_vertex_input = ddc::graphics::CreateVertexInput(cube_vertex_input_descriptor);

        ddc::graphics::VertexBufferCreateInfo quad_vb_info{};
        quad_vb_info.descriptor.static_draw = true;
        quad_vb_info.enable_init_update = true;
        quad_vb_info.init_update_info.rw_size = ddc::graphics::BUFFER_WHOLE_SIZE;
        quad_vb_info.init_update_info.memory.MakeRef(s_quad, sizeof(s_quad));

        _m_quad_vb_pos = ddc::graphics::CreateVertexBuffer(quad_vb_info);

        ddc::graphics::IndexBufferCreateInfo quad_ib_info{};
        quad_ib_info.descriptor.static_draw = true;
        quad_ib_info.enable_init_update = true;
        quad_ib_info.init_update_info.rw_size = ddc::graphics::BUFFER_WHOLE_SIZE;
        quad_ib_info.init_update_info.memory.MakeRef(s_quad_idx, sizeof(s_quad_idx));

        _m_quad_ib = ddc::graphics::CreateIndexBuffer(quad_ib_info);

        ddc::graphics::VertexInputDescriptor vertex_input_desc{};
        vertex_input_desc.n_vertex_buffers = 1;
        vertex_input_desc.p_vertex_buffer = &_m_quad_vb_pos;
        vertex_input_desc.p_index_buffer = &_m_quad_ib;

        ddc::graphics::VertexInputLayout quad_vertex_layout = {};
        vertex_input_desc.p_layout = &quad_vertex_layout;

        vertex_input_desc.p_layout->binding_state.emplace_back();
        vertex_input_desc.p_layout->binding_state.back()
                .AddAttrib(
                        ddc::graphics::EVertexAttribType::FLOAT32,
                        ddc::graphics::EVertexAttribSematic::POSITION,
                        3,
                        0)
                .AddAttrib(
                        ddc::graphics::EVertexAttribType::FLOAT32,
                        ddc::graphics::EVertexAttribSematic::TEXCOORD0,
                        2,
                        sizeof(float) * 3)
                .AutoCalcStride();

        _m_quad_vertex_input = ddc::graphics::CreateVertexInput(vertex_input_desc);
    }

    ddc::graphics::HShaderProgram CreateShader(const char *vsh_filename, const char *fsh_filename) {
        auto vsh_source = LoadFileString(vsh_filename);
        auto fsh_source = LoadFileString(fsh_filename);

        std::cout << vsh_source.data() << std::endl << ">>>><<<<" << std::endl << fsh_source.data() << std::endl;

        auto shader_program = ddcx::glx::CreateShaderProgram(vsh_source.data(), vsh_filename, fsh_source.data(), fsh_filename);
        CheckHandle(shader_program);

        return shader_program;
    }

    void BuildProgram() {
        _m_cube_shader = CreateShader("assets/cube.vert", "assets/cube.frag");
        _m_cube_shader_outline = CreateShader("assets/cube_outline.vert", "assets/cube_outline.frag");
        _m_quad_shader_program = CreateShader("assets/quad.vert", "assets/quad.frag");
    }

    void BuildUniformBufferAndBindingSet() {
        _m_cube_property_set_uniform = ddc::graphics::CreateShaderPropertySet();
        _m_cube_property_set_sampler = ddc::graphics::CreateShaderPropertySet();

        ddc::graphics::WritePropertySamplerInfo write_sampler_info{};
        write_sampler_info.sampler = _m_tex_sampler;
        write_sampler_info.texture = _m_cube_texture;
        write_sampler_info.location = ddc::graphics::NameToPropertyLocation("_tex");

        ddc::graphics::WriteShaderPropertiesInfo write_properties{};
        write_properties.prop_set = _m_cube_property_set_sampler;
        write_properties.n_sampler_write = 1;
        write_properties.p_sampler_write = &write_sampler_info;

        ddc::graphics::ShaderPropertySetsWritesInfo sets_writes{};
        sets_writes.n_writes = 1;
        sets_writes.p_writes = &write_properties;

        ddc::graphics::WriteShaderPropertySets(sets_writes);

        // QUAD

        _m_quad_property_set = ddc::graphics::CreateShaderPropertySet();
        write_sampler_info.location = ddc::graphics::NameToPropertyLocation("_tex");
        write_sampler_info.texture = _m_render_texture_color;
        write_sampler_info.sampler = _m_tex_sampler;

        write_properties.prop_set = _m_quad_property_set;

        ddc::graphics::WriteShaderPropertySets(sets_writes);

        _m_quad_property_set_ds_tex = ddc::graphics::CreateShaderPropertySet();
        write_sampler_info.location = ddc::graphics::NameToPropertyLocation("_tex");
        write_sampler_info.texture = _m_render_texture_ds;
        write_sampler_info.sampler = _m_tex_sampler;

        write_properties.prop_set = _m_quad_property_set_ds_tex;

        ddc::graphics::WriteShaderPropertySets(sets_writes);
    }

    void BuildRenderPass() {
        ddc::graphics::ColorAttachmentDescriptor bb_color;
        ddc::graphics::DepthStencilAttachmentDescriptor bb_ds;
        ddc::graphics::GetBackBufferAttachments(&bb_color, &bb_ds);

        bb_ds.stencil_load_op = ddc::graphics::EAttachmentLoadOperation::CLEAR;

        ddc::graphics::RenderPassDescriptor desc{};
        desc.p_depth_stencil_attachment = &bb_ds;
        desc.n_color_attachments = 1;
        desc.p_color_attachments = &bb_color;

        _m_cube_render_pass_to_screen = ddc::graphics::CreateRenderPass(desc);
        CheckHandle(_m_cube_render_pass_to_screen);

        // OFFSCREEN

        ddc::graphics::ColorAttachmentDescriptor color_offscreen{};
        color_offscreen.texture = _m_render_texture_color;

        ddc::graphics::DepthStencilAttachmentDescriptor ds_offscreen{};
        ds_offscreen.texture = _m_render_texture_ds;
        ds_offscreen.stencil_load_op = ddc::graphics::EAttachmentLoadOperation::CLEAR;

        desc.n_color_attachments = 1;
        desc.p_color_attachments = &color_offscreen;
        desc.p_depth_stencil_attachment = &ds_offscreen;

        _m_render_pass_offscreen = ddc::graphics::CreateRenderPass(desc);

        CheckHandle(_m_render_pass_offscreen);
    }

    void BuildGraphicsPipeline() {
        ddc::graphics::GraphicsPipelineDescriptor desc_cube{};
        desc_cube.shader_program = _m_cube_shader;
        desc_cube.vertex_input = _m_cube_vertex_input;
        desc_cube.enable_stencil_test = true;
        desc_cube.stencil_func = ddc::graphics::EStencilFunc::ALWAYS;
        desc_cube.stencil_ref = 1;
        desc_cube.stencil_ref_mask = 0xff;
        desc_cube.stencil_mask = 0xff;
        desc_cube.stencil_op_depth_pass = ddc::graphics::EStencilOperation::REPLACE;

        _m_cube_pipeline = ddc::graphics::CreateGraphicsPipeline(desc_cube);
        CheckHandle(_m_cube_pipeline);

        desc_cube = ddc::graphics::GraphicsPipelineDescriptor();

        desc_cube.shader_program = _m_cube_shader_outline;
        desc_cube.vertex_input = _m_cube_vertex_input;
        desc_cube.enable_stencil_test = true;
        desc_cube.stencil_func = ddc::graphics::EStencilFunc::NOTEQUAL;
        desc_cube.stencil_ref = 1;
        desc_cube.stencil_ref_mask = 0xff;
        desc_cube.stencil_mask = 0xff;

        _m_cube_pipeline_outline = ddc::graphics::CreateGraphicsPipeline(desc_cube);
        CheckHandle(_m_cube_pipeline_outline);

        ddc::graphics::GraphicsPipelineDescriptor desc_quad{};
        desc_quad.shader_program = _m_quad_shader_program;
        desc_quad.vertex_input = _m_quad_vertex_input;

        _m_quad_pipeline = ddc::graphics::CreateGraphicsPipeline(desc_quad);
    }

    void OnBeforeCreateWindow(ddc::window::WindowCreationInfo &window_create_info) override {
        window_create_info.flags |= ddc::window::EWindowCreationFlags::RESIZABLE;
        window_create_info.window_title = "JUST A CUBE";
        window_create_info.window_width = 1280;
        window_create_info.window_height = 720;
    }

    void InitOpenGLInfo() {
        auto&& gfx_vendor = ddc::graphics::GetGraphicsVendorInfo();
        vendor = gfx_vendor.vendor;
        renderer = gfx_vendor.renderer;
        version = gfx_vendor.version;
    }

    void OnInit() override {
        auto& io = ImGui::GetIO();
        io.FontGlobalScale = 2.0f;

        InitOpenGLInfo();
        BuildTexture();
        BuildCubeMesh();
        BuildCubeVBO();
        BuildProgram();
        BuildRenderPass();
        BuildGraphicsPipeline();
        BuildUniformBufferAndBindingSet();
    }

    static void WriteUniform(
            ddc::graphics::HShaderPropertySet set, const char *name, ddc::graphics::EUniformType type, const void *memory) {

        ddc::graphics::WritePropertyUniformInfo write_info{};
        write_info.location = ddc::graphics::NameToPropertyLocation(name);
        write_info.uniform_type = type;
        write_info.memory = memory;

        ddc::graphics::WriteShaderPropertiesInfo write_shader_props{};
        write_shader_props.prop_set = set;
        write_shader_props.n_uniform_write = 1;
        write_shader_props.p_uniform_write = &write_info;

        ddc::graphics::ShaderPropertySetsWritesInfo sets_writes{};

        sets_writes.n_writes = 1;
        sets_writes.p_writes = &write_shader_props;

        ddc::graphics::WriteShaderPropertySets(sets_writes);
    }

    void UpdateCubeUniforms(ddc::graphics::HShaderPropertySet set) {
        WriteUniform(set, "mat_vp", ddc::graphics::EUniformType::MAT4, glm::value_ptr(_m_cube_uniforms.vp));
        WriteUniform(set, "mat_model", ddc::graphics::EUniformType::MAT4, glm::value_ptr(_m_cube_uniforms.model));
        WriteUniform(set, "light_dir", ddc::graphics::EUniformType::FLOAT3, &_m_cube_uniforms.light_dir);
        WriteUniform(set, "expand_factor", ddc::graphics::EUniformType::FLOAT1, &_m_cube_uniforms.expand_factor);
        WriteUniform(set, "_time", ddc::graphics::EUniformType::FLOAT1, &_m_sys_time);
    }

    void UpdateQuadUniforms(ddc::graphics::HShaderPropertySet set) {
        WriteUniform(set, "mat_model", ddc::graphics::EUniformType::MAT4, glm::value_ptr(_m_quad_uniforms.model));
    }

    void OnUpdate() override {
        if (ddc::window::GetKeyboardState(ddc::keyboard::EScancode::SCANCODE_W)) {
            _player_z -= 2 * ddc::time::GetTimeDelta();
        }

        if (ddc::window::GetKeyboardState(ddc::keyboard::EScancode::SCANCODE_S)) {
            _player_z += 2 * ddc::time::GetTimeDelta();
        }

        if (!_m_gui_captured_mouse && ddc::mouse::GetMouseButtonState(ddc::mouse::EMouseButton::LEFT)) {
            if (_m_touch_down) {
                int dx, dy;
                ddc::mouse::GetMouseMotionDxDy(&dx, &dy);
                _local_rot_x += static_cast<float>(dy);
                _local_rot_y += static_cast<float>(dx);
            }
            _m_touch_down = true;
        } else {
            _m_touch_down = false;
            _local_rot_y += ddc::time::GetTimeDelta() * 30 * (_m_rot_speed_multiplier * _m_rot_speed_multiplier);
        }

        int ww, wh;
        ddc::window::GetWindowSize(&ww, &wh);

        glm::mat4 local_rot = glm::mat4(1.0f);
        local_rot = glm::rotate(local_rot, glm::radians(_local_rot_x), glm::vec3(1.0f, 0.0f, 0.0f));
        local_rot = glm::rotate(local_rot, glm::radians(_local_rot_y), glm::vec3(0.0f, 1.0f, 0.0f));

        glm::mat4 model = local_rot;

        glm::mat4 view = glm::lookAt(
                glm::vec3(0, 0, _player_z),
                glm::vec3(0, 0, 0),
                glm::vec3(0, 1, 0));
        glm::mat4 projection = glm::perspective(
                glm::radians(30.0f), (float) ww / (float) wh, 0.1f, 100.0f);
        glm::mat4 vp = projection * view;

        _m_cube_uniforms.model = model;
        _m_cube_uniforms.vp = vp;

        _m_sys_time = ddc::time::GetTime();

        UpdateCubeUniforms(_m_cube_property_set_uniform);
    }

    ddc_uint32 frame = 0;

    void OnRender() override {
        OffscreenPass();
        ScreenQuadPass();

        frame++;
    }

    void ScreenQuadPass() {
        int win_w, win_h;
        ddc::window::GetWindowSize(&win_w, &win_h);

        ddc::graphics::ViewportDescriptor viewport{};
        viewport.x = 0;
        viewport.y = 0;
        viewport.width = win_w;
        viewport.height = win_h;
        ddc::graphics::SetViewport(viewport);

        ddc::graphics::RenderPassBeginInfo rp_begin_info{};
        rp_begin_info.render_pass = _m_cube_render_pass_to_screen;

        ddc::graphics::BeginRenderPass(rp_begin_info);
        ddc::graphics::BindVertexInput(_m_quad_vertex_input);
        ddc::graphics::BindGraphicsPipeline(_m_quad_pipeline);

        ddc::graphics::BindShaderPropertySetsInfo sets_bind_info{};
        sets_bind_info.n_property_sets = 1;
        sets_bind_info.p_binding_sets = &_m_quad_property_set;

        ddc::graphics::BindShaderPropertySets(sets_bind_info);

        ddc::graphics::PushConstants(
                ddc::graphics::NameToPropertyLocation("_effect"),
                ddc::graphics::EUniformType::FLOAT4,
                &_m_quad_uniforms.quad_effect,
                sizeof(glm::vec4));

        ddc::graphics::PushConstants(
                ddc::graphics::NameToPropertyLocation("_disturb"),
                ddc::graphics::EUniformType::FLOAT4,
                &_m_quad_uniforms.quad_uv_disturb,
                sizeof(glm::vec4));

        if (_m_render_single_quad) {
            ddc::graphics::PushConstants(
                    ddc::graphics::NameToPropertyLocation("mat_model"),
                    ddc::graphics::EUniformType::MAT4,
                    glm::value_ptr(glm::scale(glm::mat4(1.0f), _m_quad_scale * 2.0f)),
                    sizeof(glm::mat4));
            ddc::graphics::DrawIndexed(
                    sizeof(s_quad_idx) / sizeof(s_quad_idx[0]),
                    0, 0, 0, 0);
        } else {
            auto E = glm::mat4(1.0f);

            float sin_val;
            glm::mat4 base_scale;

            auto time = ddc::time::GetTime() * 2 * _m_quad_wave.y;

            constexpr float qt_pi = 3.14159265358979323846f / 4;

            sin_val = _m_quad_wave.x * 0.1f * std::sin(time + qt_pi * 1 * _m_quad_wave.y) + (0.9 + 0.1 * (1 - _m_quad_wave.x));
            base_scale = glm::scale(E, _m_quad_scale * sin_val);

            ddc::graphics::PushConstants(
                    ddc::graphics::NameToPropertyLocation("mat_model"),
                    ddc::graphics::EUniformType::MAT4,
                    glm::value_ptr(
                            glm::translate(E, glm::vec3(0.5f, 0.5f, 0.0f)) * base_scale),
                    sizeof(glm::mat4));
            ddc::graphics::DrawIndexed(
                    sizeof(s_quad_idx) / sizeof(s_quad_idx[0]),
                    0, 0, 0, 0);

            sin_val = _m_quad_wave.x * 0.1f * std::sin(time + qt_pi * 2 * _m_quad_wave.y) + (0.9 + 0.1 * (1 - _m_quad_wave.x));
            base_scale = glm::scale(E, _m_quad_scale * sin_val);

            ddc::graphics::PushConstants(
                    ddc::graphics::NameToPropertyLocation("mat_model"),
                    ddc::graphics::EUniformType::MAT4,
                    glm::value_ptr(
                            glm::translate(E, glm::vec3(-0.5f, 0.5f, 0.0f)) * base_scale),
                    sizeof(glm::mat4));
            ddc::graphics::DrawIndexed(
                    sizeof(s_quad_idx) / sizeof(s_quad_idx[0]),
                    0, 0, 0, 0);

            sin_val = _m_quad_wave.x * 0.1f * std::sin(time + qt_pi * 3 * _m_quad_wave.y) + (0.9 + 0.1 * (1 - _m_quad_wave.x));
            base_scale = glm::scale(E, _m_quad_scale * sin_val);

            sets_bind_info.n_property_sets = 1;
            sets_bind_info.p_binding_sets = &_m_quad_property_set_ds_tex;

            ddc::graphics::BindShaderPropertySets(sets_bind_info);

            ddc::graphics::PushConstants(
                    ddc::graphics::NameToPropertyLocation("mat_model"),
                    ddc::graphics::EUniformType::MAT4,
                    glm::value_ptr(
                            glm::translate(E, glm::vec3(-0.5f, -0.5f, 0.0f)) * base_scale),
                    sizeof(glm::mat4));
            ddc::graphics::DrawIndexed(
                    sizeof(s_quad_idx) / sizeof(s_quad_idx[0]),
                    0, 0, 0, 0);

            sin_val = _m_quad_wave.x * 0.1f * std::sin(time + qt_pi * 4 * _m_quad_wave.y) + (0.9 + 0.1 * (1 - _m_quad_wave.x));
            base_scale = glm::scale(E, _m_quad_scale * sin_val);

            ddc::graphics::PushConstants(
                    ddc::graphics::NameToPropertyLocation("mat_model"),
                    ddc::graphics::EUniformType::MAT4,
                    glm::value_ptr(
                            glm::translate(E, glm::vec3(0.5f, -0.5f, 0.0f)) * base_scale),
                    sizeof(glm::mat4));
            ddc::graphics::DrawIndexed(
                    sizeof(s_quad_idx) / sizeof(s_quad_idx[0]),
                    0, 0, 0, 0);
        }

        ddc::graphics::EndRenderPass();
    }

    void OffscreenPass() {
        ddc::graphics::RenderPassBeginInfo rp_begin_info{};
        rp_begin_info.render_pass = _m_render_pass_offscreen;
        rp_begin_info.n_clear_colors = 1;
        rp_begin_info.p_clear_colors = &_m_clear_color;

        ddc::graphics::BeginRenderPass(rp_begin_info);

        ddc::graphics::ViewportDescriptor viewport{};
        viewport.x = 0;
        viewport.y = 0;
        viewport.width = _m_quad_tex_w;
        viewport.height = _m_quad_tex_h;
        ddc::graphics::SetViewport(viewport);

        ddc::graphics::HShaderPropertySet sets[] = {_m_cube_property_set_sampler, _m_cube_property_set_uniform};

        ddc::graphics::BindShaderPropertySetsInfo bind_set_info{};
        bind_set_info.n_property_sets = 2;
        bind_set_info.p_binding_sets = sets;
        ddc::graphics::BindShaderPropertySets(bind_set_info);

        ddc::graphics::BindVertexInput(_m_cube_vertex_input);
        ddc::graphics::BindGraphicsPipeline(_m_cube_pipeline);
        ddc::graphics::DrawIndexed(_m_cube_mesh.m_indices.size(), 0, 0, 0, 0);
        ddc::graphics::BindGraphicsPipeline(_m_cube_pipeline_outline);
        ddc::graphics::DrawIndexed(_m_cube_mesh.m_indices.size(), 0, 0, 0, 0);
        ddc::graphics::EndRenderPass();
    }

    void OnImGui() override {
        auto io = ImGui::GetIO();

        bool open = true;  // Always show.

        {
            _m_gui_captured_mouse = io.WantCaptureMouse;

            static float f = 0.0f;
            static int counter = 0;

            static bool first_creation = true;

            if (first_creation) {
                ImGui::SetNextWindowSize(ImVec2(0, 1300), ImGuiCond_Once);
                first_creation = false;
            }

            ImGui::Begin(u8"Dodoco Demo: [Cube]", &open);

            ddc::window::GetMousePosition(&mx, &my);

            ddc_int32 dx = mx - lmx, dy = my - lmy;
            if (dx == 0 || dy == 0) {
                mouse_update_frame++;
            } else {
                mouse_update_frame = 0;
            }

            ImGui::Text("System Time: %f", _m_sys_time);

            ImGui::Text("Player Z");
            ImGui::SliderFloat("##player_z", &_player_z, 1.0f, 8.0f);

            ImGui::SeparatorText("Quad Control");

            ImGui::Checkbox("Single Quad", &_m_render_single_quad);

            ImGui::Text("Quad Scale");
            ImGui::SliderFloat2("##quad_scale", glm::value_ptr(_m_quad_scale), 0.0f, 1.0f);
            ImGui::Text("Quad Disturb X");
            ImGui::SliderFloat("##quad_disturb_x", glm::value_ptr(_m_quad_uniforms.quad_uv_disturb), 0.0f, 2.0f);
            ImGui::Text("Quad Disturb Y");
            ImGui::SliderFloat("##quad_disturb_y", glm::value_ptr(_m_quad_uniforms.quad_uv_disturb) + 1, 0.0f, 0.5f);
            ImGui::Text("Quad Disturb Z");
            ImGui::SliderFloat("##quad_disturb_z", glm::value_ptr(_m_quad_uniforms.quad_uv_disturb) + 2, 1.0f, 5.0f);
            ImGui::Text("Quad Effect");
            ImGui::SliderFloat("##quad_effect_x", glm::value_ptr(_m_quad_uniforms.quad_effect), 0.0f, 3.0f);
            ImGui::SliderFloat3("##quad_effect_yzw", glm::value_ptr(_m_quad_uniforms.quad_effect) + 1, 0.0f, 1.0f);

            if (!_m_render_single_quad) {
                ImGui::Text("Quad Wave Param");
                ImGui::SliderFloat3("##quad_wave_speed", glm::value_ptr(_m_quad_wave), 0.0f, 1.0f);
            }

            ImGui::SeparatorText("Cube Control");
            ImGui::Text("Clear Color");
            ImGui::ColorEdit3("", (float*) &_m_clear_color);
            ImGui::Text("Light Dir");
            ImGui::SliderFloat3("##light_dir", (float*) &_m_cube_uniforms.light_dir, -1.0f, 1.0f);
            ImGui::Text("Rotate Speed Multiplier (x^2)");
            ImGui::SliderFloat("##rot_speed_multiplier", &_m_rot_speed_multiplier, 0.0f, 3.0f);
            ImGui::Text("Expand Factor");
            ImGui::SliderFloat("##expand_factor", &_m_cube_uniforms.expand_factor, 0.8f, 1.2f);
            ImGui::Text("\n");

            ImGui::TextWrapped("Mouse Position: (%d, %d), delta = (%03d, %03d), upd = %d", mx, my, dx, dy, mouse_update_frame);

            ImGui::TextWrapped(u8"Avg. FPS: %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate);
            ImGui::Text("\n");
            ImGui::TextWrapped("OpenGL version info:\n");
            ImGui::TextWrapped("Vendor: %s\n", (const char *) vendor);
            ImGui::TextWrapped("Renderer: %s\n", (const char *) renderer);
            ImGui::TextWrapped("Version: %s\n", (const char *) version);

            ImGui::End();
        }

        lmx = mx, lmy = my;
    }
} cube_demo;

extern "C" int SDL_main(int argc, const char *args[]) {
    return ddc::app::RunApp(cube_demo);
}
